iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 24
0
自我挑戰組

Android Architecture 及 Unit Test系列 第 24

[Day 24] Test:Part 6 Fragment

  • 分享至 

  • xImage
  •  

昨天已經把平時 UI 測試會用到的東西都稍微介紹過了,今天會開始為 TasksFragment 建立測試。

如果有在使用 Dagger 的話就會遇到一個問題,所以執行時會有一些問題。

至於是什麼問題呢?

  1. 現在的 Fragment 及 Activity 已改為繼承自 DaggerFragmentDaggerAppCompatActivity 而不是原生的 Android 組件
  2. 重新思考 DI 的原理,我們需要將 instance 注入依賴者中,但此時依賴者變成了 Activity/Fragment ,他們既沒有 constructor 且依賴的對象是一堆 Android 的元件

一般解決這個問題是寫另外的 Module 和 Component 來模擬 DI ,我們先從這裡開始吧。

測試前的準備

首先要先創建一個測試用的 Application :

class TestTodoApplication : TodoApplication(), HasSupportFragmentInjector {

    lateinit var fragmentInjector: DispatchingAndroidInjector<Fragment>

    override fun supportFragmentInjector() = fragmentInjector
}

當然,由於我們使用 AndroidJUnitRunner 執行 Android Test ,他使用的是原始的 Application ,所以也要建立一個自定義的 Runner 執行新的測試:

class CustomTestRunner : AndroidJUnitRunner() {

    override fun newApplication(
        cl: ClassLoader?,
        className: String?,
        context: Context?
    ): Application {
        return super.newApplication(cl, TestTodoApplication::class.java.name, context)
    }
}

替換掉原來的 AndroidJUnitRunner:

// app/build.gradle
android {
    defaultConfig {
        ......
        testInstrumentationRunner 'com.ininmm.todoapp.CustomTestRunner'
    }
}

重新設計 Test 用的 Module 及 Component:

@Module
class TestApplicationModule {

    @Singleton
    @Provides
    fun provideIoDispatcher() = Dispatchers.IO
    
    @Provides
    @Singleton
    fun provideRepository(): ITasksRepository = FakeRepository()
}

@Singleton
@Component(
    modules = [
        TestApplicationModule::class,
        AndroidSupportInjectionModule::class,
        TasksActivityBinds::class,
        TasksModule::class,
        TaskDetailModule::class,
        StatisticsModule::class,
        TasksModule::class,
        ViewModelBuilder::class
    ]
)
interface TestApplicationComponent : AndroidInjector<TestTodoApplication> {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance applicationContext: Context): TestApplicationComponent
    }

    val tasksRepository: ITasksRepository
}

寫一個 rule 來把 application 傳給 Dagger :

class DaggerTestApplicationRule : TestWatcher() {

    lateinit var component: TestApplicationComponent
        private set

    override fun starting(description: Description?) {
        super.starting(description)

        val app = ApplicationProvider.getApplicationContext<Context>() as TestTodoApplication
        component = DaggerTestApplicationComponent.factory().create(app)
        component.inject(app)
    }
}

這樣就先準備好測試前的東西了。

寫 Fragment 的測試

一樣會先在開始時初始化一些必要的東西:

class TasksFragmentTest {

    private lateinit var repository: ITasksRepository

    @get:Rule
    val rule = DaggerTestApplicationRule()

    @MockK(relaxUnitFun = true)
    private lateinit var navController: NavController

    @Before
    fun setupDaggerComponent() {
        MockKAnnotations.init(this)
        repository = rule.component.tasksRepository
        runBlocking { 
            repository.deleteAllTasks()
        }
    }
}

這邊我只舉一些範例,如果想測試 Activity 上點擊 Menu 選擇 Filter Type 後是否會顯示對應的 UI ,一樣也是基於 Given-When-Then 的概念:

  1. 準備好進入 Activity
  2. 採取某些動作
  3. 驗證 UI 是否正確

如下表示:

class TasksFragmentTest {

    ......
    @Test
    fun displayCompletedTask() {
        // Given
        repository.saveTaskBlocking(createTasks()[1])
        // When 
        launchActivity()

        onView(withText("Title2")).check(matches(isDisplayed()))

        onView(withId(R.id.menu_filter)).perform(click())
        onView(withText(R.string.nav_active)).perform(click())
        // Then
        onView(withText("Title2")).check(matches(not(isDisplayed())))

        onView(withId(R.id.menu_filter)).perform(click())
        onView(withText(R.string.nav_completed)).perform(click())
        // Then
        onView(withText("Title2")).check(matches(isDisplayed()))
    }
}

還有一個比較複雜的情況,點擊某個按鈕進入 Tasks 新增頁,以上很多行為都是 Android API 控制,所以我這裡就只針對某幾個關鍵方法做是否調用的檢查:

class TasksFragmentTest {

    ......
    @Test
    fun clickAddTaskButtonThenNavigateToAddEditFragment() {
        // GIVEN - 在 TasksFragment
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - 點擊 "+" button
        onView(withId(R.id.taskFabAddTask)).perform(click())

        // THEN - 驗證調用方法 navigate 到 AddEditTaskFragment
        val navDirections = TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
            null,
            getApplicationContext<Context>().getString(R.string.add_task)
        )
        every { navController.navigate(navDirections) } just Runs
        verify { navController.navigate(navDirections) }
    }
}

以上就是 Fragment 測試的簡單介紹,明天會繼續完成 Activity 的測試。


上一篇
[Day 23] Test:Part 5 UI Test
下一篇
[Day 25] Test:Part 7 Activity
系列文
Android Architecture 及 Unit Test30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言